% -*- coding: utf-8 -*-
% Ada 95 品质和风格
% 版权所有 (C) 2011,2012 朱群英
% Copyright (C) 2011,2012 Zhu Qun-Ying
%
% 本书的 TeX 代码和由之生成的 ps，pdf，html，等其他格式的文件
% 遵循 GNU通用公共授权第三版或其后的版本。
%
% 本书是基于有益的目的而加以发布，然而不负任何担保责任。
%
% 您应已收到附随于本书的GNU通用公共授权的副本；如果没有，
% 请参考 <http://www.gnu.org/licenses/gpl.html>

\section{高级结构}
\begin{blockindent}
构建良好的程序很同意被理解、提高和维护。构建糟糕的程序在维护时，
经常需要重新构建以方便工作。下面列出的许多准则，是作为通用程序设计准则
给出的。
\end{blockindent}

\subsection{分别编译能力}
\label{c:prog-struct:high-level:separate-comp}
\glentry{准则}
\begin{itemize}
\item 把每一个库单元的规约放在独立于实体的文件中。
\item 避免定义不是主程序的库单元子程序。如果定义了这样的子程序，
对每一个库单元子程序创建一个明确的单独的规约文件。
\item 把亚单元的使用降到最低。
\item 对于亚单元而言，用子库单元来把一个子系统构建为可管理的多个单元。
\item 每一个亚单元存放在独立的文件中。
\item 使用一致的文件命名约定。
\item 对于包主体的嵌套，用私有子包并在父包中引用。
\item 对于(其他)子单元用于扩展父单元的抽象或服务需要的数据和子程序，
 用私有子单元规约。
\end{itemize}

\glentry{范例}
\begin{blockindent}
下面的文件名说明了一种可能的文件组织以及相随的一致命名约定。
库单元的名字用后缀 adb 来表示单元主体，后缀 ads 表示单元规约，
任何其他包含亚单元的文件的名字，由体名字和亚单元名字以下划线
分隔:
\begin{lstlisting}
text_io.ads                 -- the specification
text_io.adb                 -- the body
text_io_integer_io.adb      -- a subunit
text_io_fixed_io.adb        -- a subunit
text_io_float_io.adb        -- a subunit
text_io_enumeration_io.adb  -- a subunit
\end{lstlisting}

依赖于文件系统允许文件名中可以用什么字符，你可以在文件
名字中用更清晰的方式来区分父和亚单元的名字。
如果文件系统允许使用 \texttt{"\#"} 字符，那可以用 \texttt{\#}
来分隔主体和亚单元的名字:
\begin{lstlisting}
text_io.ads                 -- the specification
text_io.adb                 -- the body
text_io#integer_io.adb      -- a subunit
text_io#fixed_io.adb        -- a subunit
text_io#float_io.adb        -- a subunit
text_io#enumeration_io.adb  -- a subunit
\end{lstlisting}

有些操作系统区分大小写，尽管 Ada 本身是不区分大小写的语言。
这时，可以选择全用小写来表示文件名。
\end{blockindent}

\glentry{原理}
\begin{blockindent}
本准则侧重在分散文件上的主要原因，是最小化每次改动后需要重新编译的
文件数。通常，在软件开发过程中，主体的改动要比规约要频繁得多。
如果规约和主体在同一个文件中，那么每次主体的编译也会编译一次规约，
虽然规约并没有改变。 因为规约定义了单元和其所有使用者之间的接口，
每次规约的重编译都会导致所有使用者必须重编译以确认符合规约。如果
使用者的规约和主体也在一个文件里，那任何使用这些单元的使用者也要
重编译，以此类推。这个涟漪效应，会强制大量本可以避免的重编译，严重
拖慢了项目的开发和测试阶段。这就是为什么你要把所有库单元 (非嵌套单元)
中的规约和其主体分别存放在不同的文件中。

库单元中子程序要尽量短小。库单元子程序的真正运用只有作为主子程序时。
几乎在其他任何情况下，把子程序嵌套在包中会更好些。这为子程序在主体中
提供了一个本地化所需数据的场所。而且，这也减低了系统中单独模块的数量。

通常，对于任何用 \texttt{with} 语句引用的库中的子程序，都应有一个单独
的规约。这让使用的单元只依赖于库子程序的规约，而不是其主体。

亚单元的使用也应该尽量的少，因为他们产生维护问题。父主体中的声明在
亚单元中是可见的，这增加了亚单元中的全局数据，因此提高了改动带来的潜在
涟漪效应。亚单元阻碍了代码的重用，因为他提供了把可能重用的代码直接放在
亚单元的便利，而不是把他们放在一个通用的例程以供其他子程序调用。

Ada 95 提供了子库单元的功能，可以避免大多数亚单元的使用。例如，
用子库单元和对应的语境语句来替代亚单元庞大而嵌套的主体。子库单元
主体的改变不会引起子系统中其他单元的重编译。

另一个使用多个独立文件的好处是，不同的程序员可以使用普通的编辑器
同时修正系统的不同部分，一般的编辑器不允许同时对一文件进行多处
修改。

最后，将规约和主体分离可以实现一个规约对应多个主体或多个规约对应一个
主体。虽然 Ada 要求在一个系统中的任何时间，一个主体只能有一个规约，
但是维护多主体或多规约对于系统的不同构建仍然有好处。例如，一个规约可以有
多个主体，每个主体都实现相同的功能，但是在时间和空间的折衷不同，
或者是依赖机器的代码，每一个目标机器都有一个对应的主体。维护多规约在开发
和调试阶段也很有用。例如，一个规约提供给客户，另一个给单元测试。前一个
只导出在正常操作下外部可以使用的子程序，后一个导出所有的子程序，
以便独立测试每一个子程序。

一致的文件命名约定是值得推荐的，这有利于管理由于遵循本准则而产生的大量文件。

在实现包规约中的抽象时，通常需要写一些支持的子程序来操纵内部数据。
不要在接口中导出这些子程序。 可以选择把他们放在父包的主体中，或者父包主体
中语境语句引用的子包中。当放在父包的主体中时，所有使用父包的客户都不能
访问他们，包括扩展父包的子包也不可以。如果实现父包抽象的扩展时，
要用到这些子程序，父包的规约和主体都将被迫修改，因为扩展必须要在父包规约
中声明。这个方法就会引起整个包 (规约和主体) 和所有客户的重编译。

也可以用私有子包来实现这些支持子程序。因为父单元的规约没有更改，他和他
的客户都不需要重编译。本来要在父单元主体中声明的数据和子程序，改在
私有子单元的规约中声明，这样他们在父单元主体和任何扩展父单元服务和抽象的
子单元中都可见。(参见准则\ref{c:prog-struct:high-level:child-lib-unit} 和
\ref{c:prog-struct:visibility})。私有子单元的使用通常会将本单元家族
及其客户重编译的次数变得最少。

当声明子包为私有时，相当于在父包主体中声明的效果，父包的客户不可以用
语境语句来指定私有子包。这么做很有灵活性，用子包来扩展父包的抽象时，
如果没有修改父包的规约和主体，那就不用重编译父包。这个灵活性通常会用来
抵消因此而增加的单元之间的依赖性，在这里指父主体或其他子包主体中用于
指定私有子包的多出来的语境语句。
\end{blockindent}

\subsection{配置编译指示}
\glentry{准则}
\begin{itemize}
    \item 尽可能通过编译器参数或其他不需要修改代码的形式来配置编译指示。
    \item 当配置编译指示必须放在代码中时，考虑在每一个分区中把他们隔离
在一个编译单元里，如果要指定，推荐用分区的主子程序。
\end{itemize}

\glentry{原理}
\begin{blockindent}
编译指示一般是作为分区或系统范围的参数。通常，他们反应高级别
软件架构的决策 (例如: pragma Task\_Dispatching\_Policy)
或者在某特定应用领域之使用 (例如: 安全关键软件)。
如果编译指示内嵌在某个软件组件中，当组件在配置并不适用的环境中重用，
就会在新软件中产生问题。这包括被编译系统拒绝或运行时有意外的行为。
由于编译指示的影响域比较大，这些问题可能会很显著。另外，系统维护可能
需要修改某些系统范围的配置。如果这些配置散落在系统中，将很难找出需要
修改的地方。

因此，推荐尽可能的把所有编译指示放在一个编译单元中，以便于查找和修改。
如果这个编译单元不大可能被重用 (例如：主子程序)，那么和将来重用时的冲突
随之减少。最后，如果系统范围的指示可以不必内嵌在代码中，如通过编译器参数，
那以上所述问题发生的可能性就更少了。
\end{blockindent}

\glentry{异例}
某些编译指示 (例如: pragma Suppress) 有不同形式的使用，包括作为
一个编译指示。本准则不适用于那些不作为指示时的使用。

\subsection{子程序}
\glentry{准则}
\begin{itemize}
\item 用子程序来增加抽象度。
\item 限制每个子程序只执行单一动作。
\end{itemize}

\glentry{范例}
\begin{blockindent}
作为一个菜单推动的用户界面包中的一部分，要求画出一个菜单的用户选项。
菜单的内容会根据用户状态而改变，此时用一个子程序来画比较恰当。
这样，输出的子程序只有一个目的，而决定菜单内容的逻辑在别的地方。

\begin{lstlisting}
...
----------------------------------------------------------------------
procedure Draw_Menu
      (Title   : in    String;
       Options : in    Menu) is
   ...
begin  -- Draw_Menu
   Ada.Text_IO.New_Page;
   Ada.Text_IO.New_Line;
   Ada.Text_IO.Set_Col (Right_Column);
   Ada.Text_IO.Put_Line (Title);
   Ada.Text_IO.New_Line;
   for Choice in Alpha_Numeric loop
     if Options (Choice) /= Empty_Line then
         Valid_Option (Choice) := True;
         Ada.Text_IO.Set_Col (Left_Column);
         Ada.Text_IO.Put (Choice & " -- ");
         Ada.Text_IO.Put_Line (Options (Choice));
     end if;
     ...
   end loop;
end Draw_Menu;
----------------------------------------------------------------------
\end{lstlisting}
\end{blockindent}

\glentry{原理}
\begin{blockindent}
子程序是非常高效和已知的抽象技巧。子程序通过隐藏某个活动的细节，提高了
程序的可读性。子程序并不是需要被调用一次以上才能存在。
\end{blockindent}

\glentry{注解}
\begin{blockindent}
准则\ref{c:improv-perf:pragma:inline} 探讨子程序调用的额外开销。
\end{blockindent}

\subsection{函数}
\glentry{准则}
\begin{itemize}
\item 当子程序的主要目的是提供单一值时，用函数。
\item 最小化函数的副作用。
\item 当值不需要是静态时，考虑用无参数函数。
\item 当导出类型需要继承基类型的值时，用无参数函数 (不要用常数)。
\item 如果值本身有可能被改变，用无参数函数。
\end{itemize}

\glentry{范例}
\begin{blockindent}
虽然从一个文件中读取一个字符会改变下一个将要读取的字符，和下面函数的主要目的
比起来，这是可接受的副作用:
\begin{lstlisting}
function Next_Character return Character is separate;
\end{lstlisting}
然而，如此使用函数可能会导致隐蔽的问题。任何时候，当计算顺序未定，那函数
返回值的顺序就未定。在以下这个例子中，字符放入 Word 中的顺序和接下来的
两个给 Suffix 参数的字符顺序未定。没有一个 Next\_Character 函数的实现
可以保证那个字符赋值给谁:
\begin{lstlisting}
   Word : constant String := String'(1 .. 5 => Next_Character);
begin  -- Start_Parsing
   Parse(Keyword => Word,
         Suffix1 => Next_Character,
         Suffix2 => Next_Character);
end Start_Parsing;
\end{lstlisting}

当然，如果顺序并不重要 (如随机数生成器)，那计算顺序也不重要。

下面的例子展现了用无参数函数代替常量:
\begin{lstlisting}
type T is private;
function Nil return T;       -- This function is a derivable 
function Default return T;   -- operation of type T. Also derivable, 
                             -- and the value can be changed by
                             -- recompiling the body of the function
\end{lstlisting}
也可以用常量来写:
\begin{lstlisting}
type T is private;
Nil : constant T;
Default : constant T;
\end{lstlisting}
\end{blockindent}

\glentry{原理}
\begin{blockindent}
改变任何非子程序内的变量都是副作用。这包括函数本身调用别的子程序或入口
时改变的变量在返回时改变被保持了。副作用很难被理解和维护，因此要反对。
另外，Ada 语言没有定义表达式或作为子程序参数的函数计算顺序，因此，
依赖于函数副作用计算顺序的的程序是错误的。避免在任何地方使用副作用。
\end{blockindent}

\subsection{包}
\glentry{准则}
\begin{itemize}
\item 用包来实现信息隐藏。
\item 用含有标签类型和私有类型的包来实现抽象数据类型。
\item 用包把相关的类型和对象定义组在一起
      (例如，两个或更多库单元的相同声明部分)。
\item 将机器依赖封装在包中。将某个设备的软件接口放在一个包中，以便更换
到另一种设备。
\item 将底层实现的决策或接口放在包中的子程序。
\item 用包和子程序来封装和隐藏可能改变的的程序细节 (\cite{nissen84})。
\end{itemize}

\glentry{范例}
\begin{blockindent}
读取文件名和外部文件的其他属性非常依赖于系统。一个名为 Directory 的包，
可包含了支持对外部目录及其文件的通用操作的类型和子程序。他的内部则依次
依赖于其他更加针对硬件或操作系统的包:
\begin{lstlisting}
package Directory is

   type Directory_Listing is limited private;

   procedure Read_Current_Directory (D : in out Directory_Listing);

   generic
      with procedure Process (Filename : in String);
   procedure Iterate (Over : in Directory_Listing);

   ...

private

   type Directory_Listing is ...

end Directory;

---------------------------------------------------------------

package body Directory is

   -- This procedure is machine dependent
   procedure Read_Current_Directory (D : in out Directory_Listing) is separate;


   procedure Iterate (Over : in Directory_Listing) is
      ...
   begin
      ...

      Process (Filename);

      ...
   end Iterate;

   ...

end Directory;
\end{lstlisting}
\end{blockindent}

\glentry{原理}
\begin{blockindent}
包是 Ada 中主要的构造设施。可直接支持抽象、信息隐藏和模块化。例如，为了辅助
可移植性，而把系统依赖部分封装的时候很有用。一个规约可有多个主体，以隔离
特定实现的信息，这样其的代码就不需要改变了。

把可能会有变化的地方封装起来，去掉了系统无关部分对其的不必要依赖，从而
最小化了需要实现改变的精力。
\end{blockindent}

\glentry{注解}
\begin{blockindent}
对这个准则最大反对声通常和性能受损有关。参见准则
\ref{c:improv-perf:pragma:inline} 有关子程序开销的讨论。
\end{blockindent}

\subsection{子库单元}
\label{c:prog-struct:high-level:child-lib-unit}
\glentry{准则}
\begin{itemize}
\item 如果一个新库单元是某个初始抽象的逻辑上的扩展，那定义为子库单元。
\item 如果一个库单元是独立的 (例如，导入新的抽象，只部分依赖于现存抽象)，
那么封装在一个分离的库单元中。
\item 用子包来实现子系统。
\item 用公共子单元来实现那些子系统中客户可见部分。
\item 用私有子单元来实现那些子系统中客户不可见部分。
\item 只有在实现包规约时，用私有子单元来实现局部声明。
\item 用子包来实现构造器，即使他们的返回值是访问类型。
\end{itemize}

\glentry{范例}
\begin{blockindent}
下面这个有关视窗系统的例子，是从 \cite{cohen93} 中抽取出来的，
他说明了在设计子系统时子单元的使用。父 (根) 包声明了客户和子系统需要的
类型，子类型和常量。每个子包则提供了视窗系统中某个特定的抽象部分，
例如原子、字体、图形输出、光标和键盘信息:
\begin{lstlisting}
package X_Windows is
   ...
private
   ...
end X_Windows;


package X_Windows.Atoms is
   type Atom is private;
   ...
private
   ...
end X_Windows.Atoms;


package X_Windows.Fonts is
   type Font is private;
   ...
private
   ...
end X_Windows.Fonts;


package X_Windows.Graphic_Output is
   type Graphic_Context is private;
   type Image is private;
   ...
private
   ...
end X_Windows.Graphic_Output;


package X_Windows.Cursors is
   ...
end X_Windows.Cursors;


package X_Windows.Keyboard is
   ...
end X_Windows.Keyboard;
\end{lstlisting}
\end{blockindent}

\glentry{原理}
\begin{blockindent}
用户在需要时用子库单元来扩展接口，能创造出更严谨和不杂乱的接口。
父包只有相关的功能。父包提供了通用的接口，而子单元在提供更完整的
编程接口，是对他们扩展或定义的抽象量身定做的。

子包是建立在 Ada 模块化的优势上的，``分开的规约和主体，分隔了用户对包的使用
接口(规约)和包的实现(主体) '' (\cite{rational95}, \S{}II.7)。
子包增加了既能扩展一个父包又不需要重编译父包或父包的客户的能力。

逻辑上不同的包可通过子包共享私有类型。可见度规则允许子规约的私有部分和
子主体可见父包的私有部分。因此，可避免仅仅为了开发抽象需要共享一部分私有
类型及其表征而创建一个整体的包。包的客户看不到私有的表征，因此
其子包和本身的抽象得以维持。

私有子包可以用作局部声明，这让你在需要实现父包及其扩展包时，拥有支持性的声明。
使用一套通用的支持声明(数据结构、数据操作子程序)，提高了程序的可维护性。
你可以修改内部的表征和支持子程序的实现而不需要修改或重编译其他子系统，
因为这些支持性的子程序在私有子包的主体中实现。参见准则
\ref{c:prog-struct:high-level:separate-comp}、
\ref{c:prog-struct:visiblity:min-intf}、
\ref{c:reusability:independence:subsystem-design} 和
\ref{c:reusability:independence:tagged-type-hierachies}。

另参见准则\ref{c:oof:tagged:visibility:derived-tag}
有关用子库单元来生成标签类型分层结构的讨论。
\end{blockindent}

\subsection{内聚性}
\label{c:prog-structure:high-level:cohesion}

\glentry{准则}
\begin{itemize}
\item 让每个包只用于一个目的。
\item 用包把相关的数据、类型和子程序组在一起。
\item 避免收集没有关系的对象和子程序 (\cite{nasa87}, \cite{nissen84})。
\item 考虑系统的重构, 把两个紧密相关的单元合并到一个包或包的分层结构中，
又或者把相对独立的单元分到不同的包里。
\end{itemize}

\glentry{范例}
\begin{blockindent}
这是一个反面的例子，一个名为 \texttt{Project\_Definitions} 的包，明显是
某个项目中包罗所有的包，也可能很混乱。这可能是为了让项目成员在他们的程序
中只用一个 \texttt{with} 语句而形成的。

好的例子，名为 \texttt{Display\_Format\_Definitions} 的包，包含了针对某个
特定显示的特定格式的所有类型和常量, 名为 \texttt{Cartridge\_Tape\_Handler}
的包，包含了针对某特殊用途设备的所有类型、常量和子程序。
\end{blockindent}

\glentry{原理}
\begin{blockindent}
包内实体间关系的深浅程度，对于包及其组成的程序是否易于理解有直接的影响。
组合的条件有很多，某些条件没有另一些条件有效。按照类的数据、活动(例如初始化模块)
的时间特性来分组，就没有按他们的功能或数据交流的需求分来得有效
(参加 \cite{charette86})。

``正确'' 的系统结构对于系统的维护性有巨大的影响。如果初始的结构不是很理想，
结构重组是很重要的，虽然当时会比较痛苦。

有关异构数据参见\ref{c:prog-prac:data:heterogeneous}。
\end{blockindent}

\glentry{注解}
\begin{blockindent}
传统的子例程库通常把不同功能的子例程组在一起，即使这些库应该分成包的组合，
每个包在包含一套逻辑上内聚的子例程。
\end{blockindent}

\subsection{数据耦合}
\glentry{准则}
\begin{itemize}
\item 避免在包规范中定义变量。
\end{itemize}


\glentry{范例}
\begin{blockindent}
这个例子是编译器中的一部分。处理错误信息的包和含有代码生成器的包都
需要知道当前的行数。这里并没有定义一个\texttt{Natural}的变量来分享
这一信息，而是把具体实现隐藏在包中，并提供了一个访问函数:

\begin{lstlisting}
-------------------------------------------------------------------------
package Compilation_Status is
   type Line_Number is range 1 .. 2_500_000;
   function Source_Line_Number return Line_Number;
end Compilation_Status;
-------------------------------------------------------------------------
with Compilation_Status;
package Error_Message_Processing is
   -- Handle compile-time diagnostic.
end Error_Message_Processing;
-------------------------------------------------------------------------
with Compilation_Status;

package Code_Generation is
   -- Operations for code generation.
end Code_Generation;
-------------------------------------------------------------------------
\end{lstlisting}
\end{blockindent}

\glentry{原理}
\begin{blockindent}
紧密耦合的程序单元非常难调试和维护。用访问函数来保护共享的数据，耦合度就
减少了。这避免了对于数据结构的依赖，也控制了数据访问。
\end{blockindent}

\glentry{注解}
\begin{blockindent}
最普遍的反对声音通常都牵涉到性能损失。当把一个变量搬到包实体中，就必须提供访问
子程序，每次调用都是额外开销。有关子程序调用的额外开销讨论，
参见准则\ref{c:improv-perf:pragma:inline}。
\end{blockindent}

\subsection{任务}
\glentry{准则}
\begin{itemize}
\item 在问题域中用任务来对抽象、异步的实体建模。
\item 用任务定义多处理器架构中的并行算法。
\item 用任务执行并行、循环或排序的事件 (\cite{nasa87})。
\end{itemize}

\glentry{原理}
\begin{blockindent}
这些准则的原理在准则\ref{c:concurrency:options:task}中。第\ref{c:concurrency}章
会对任务进行更深入的讨论。
\end{blockindent}


\subsection{保护类型}
\glentry{准则}
\begin{itemize}
\item 用保护类型来控制或同步对数据或设备的访问。
\item 用保护类型来实现任务间的同步，如被动的资源监视。
\end{itemize}

\glentry{范例}
\begin{blockindent}
参见准则\ref{c:concurrency:options:protected}。
\end{blockindent}

\glentry{原理}
\begin{blockindent}
这些准则的原理在准则\ref{c:concurrency:options:protected}中。
第\ref{c:concurrency}章对并行性和保护类型有更深入的讨论。
\end{blockindent}
